Skip to content

feat(incentives): add interface#1678

Merged
0xClandestine merged 1 commit intorelease-dev/incentive-councilfrom
feat/interface-design
Dec 17, 2025
Merged

feat(incentives): add interface#1678
0xClandestine merged 1 commit intorelease-dev/incentive-councilfrom
feat/interface-design

Conversation

@0xClandestine
Copy link
Copy Markdown
Member

Motivation:

We need approval on initial design of the upcoming incentives council feature release. Historically, we've used EigenHopper for token emissions. Now we're looking to make some improvements.

Modifications:

  • Drafted a consolidated interface that combines the two EigenHopper contracts into a single new contract.

Result:

Single consolidated interface with hopper functionality and proposed ELIP-012 functionality.

Comment on lines +130 to +134
/// @notice Removes a distribution.
/// @dev Only the Incentive Council can call this function.
/// @dev Ref: Implied by "updateDistribution" and general management of distributions.
/// @param index The index of the distribution to remove.
function removeDistribution(uint256 index) external;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open question: Should this mark the distribution as "removed" or outright remove the distribution from storage. Intuition tells me we should simply mark it as removed so it can still be easily audited/introspected in the future (and avoids need for swap/pop enumerable set pattern).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking implementation wise, we simply set the weight of the distribution to mark it cancelled, alternatively can just have another mapping.

RewardsForAllEarners,
OperatorSetTotalStake,
OperatorSetUniqueStake,
EigenDA,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't EigenDA just one of the old reward types? createAVSRewardsSubmission

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My interpretation is the intent of that ref is to give the possible types of rewards submissions that can be made? Could be wrong though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discuss the approach here in this doc (reference)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. If this becomes too complex in the EmissionsController, it might be worth just adding another createAVSRewardsSubmission with the AVS parameter in the RewardsCoordinator. There would be no sidecar changes here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we'd use the exact same event/logic. Don't think it's unreasonable bloat

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well there would be a sidecar change in this case due to the additional parameter right?

struct Distribution {
uint256 weight;
DistributionType distributionType;
bytes strategiesAndMultipliers;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an array? Have used that in the past

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +130 to +134
/// @notice Removes a distribution.
/// @dev Only the Incentive Council can call this function.
/// @dev Ref: Implied by "updateDistribution" and general management of distributions.
/// @param index The index of the distribution to remove.
function removeDistribution(uint256 index) external;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed

/// @dev Ref: "Incentive Council Functions: addDistribution(weight{int}, distribution-type{see below}, strategiesAndMultipliers())"
/// @param weight The weight of the distribution.
/// @param distributionType The type of distribution.
/// @param strategiesAndMultipliers Encoded strategies and multipliers.
Copy link
Copy Markdown
Contributor

@ypatil12 ypatil12 Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be any error handling on the weight? I assume it's a proportion in BPS?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah should've specified in comments, weights are in bips (since easier to manually write out vs wad).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on BPS

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/// @notice Configuration for the EmissionsController.
/// @dev Ref: "The amount of EIGEN minted weekly (inflation rate) is set by governance..."
Copy link
Copy Markdown
Contributor

@ypatil12 ypatil12 Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my understanding of the ELIP is that we have:

  1. A top-level inflation rate (eg 8%)
  2. Either eth-directed, Eigen-directed, or discretionary reward (eg. x% to ETH)
  3. A per-DistributionType type of above. x1% to rewards all stakers

How is 2 specified? It seems like we only do 3?

Copy link
Copy Markdown
Contributor

@non-fungible-nelson non-fungible-nelson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 is no longer specified. Just doing (3) is fine.

The intention is that the Incentive Council has a single bucket that it pulls from to direct emissions using the strategies and multipliers from Distribution. If there are additional commitments to be made around proportions of supply, they have to be encoded offchain or using strats and mults and the total values that end up being emitted.

/// @notice Triggers the weekly emissions.
/// @dev Ref: "The ActionGenerator today is a contract ... that is triggered by the Hopper. When triggered, it mints new EIGEN tokens..."
/// @dev Permissionless function that can be called by anyone when `canPress()` returns true.
function pressButton() external;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have to batch this if the size of the distribution array becomes too large... something to think about for implementation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout, yeah will need to ensure the button is always "pressable".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment noting we should use pagination to avoid DOS/OOG.

/// @notice Triggers the weekly emissions.
/// @dev Ref: "The ActionGenerator today is a contract ... that is triggered by the Hopper. When triggered, it mints new EIGEN tokens..."
/// @dev Permissionless function that can be called by anyone when `canPress()` returns true.
function pressButton() external;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what happens if one of the many reward tx's fail?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a try-catch for that or do we ensure it doesn't fail at all? How does the current Hopper handle that?

Copy link
Copy Markdown
Contributor

@non-fungible-nelson non-fungible-nelson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MC1823315 calls this out... Try-Catch (or error handling) is necessary to ensure minting does not revert (push button succeeds).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure that wallets will properly estimate this. We've had a bunch of problems with slashing when it came to using a try-catch

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this relevant for pressButton() though? Shouldn't this primarily be called by a cronjob that can set an arbitrarily large gas limit?

/// @notice Configuration for the EmissionsController.
/// @dev Ref: "The amount of EIGEN minted weekly (inflation rate) is set by governance..."
struct EmissionsConfiguration {
uint256 inflationRate;
Copy link
Copy Markdown
Contributor

@non-fungible-nelson non-fungible-nelson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a global fixed value not settable by the Incentives Council, only by Protocol Council ELIP. settable below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're now constant values, only modifiable via upgrades.

struct EmissionsConfiguration {
uint256 inflationRate;
uint256 startTime;
uint256 cooldownSeconds;
Copy link
Copy Markdown
Contributor

@non-fungible-nelson non-fungible-nelson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will also be global values that only allow the button to be pressed once a week on emissions. They should explicitly not be settable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're now constant values, only modifiable via upgrades.

Copy link
Copy Markdown
Contributor

@ypatil12 ypatil12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just added some clarifying comments

OperatorSetTotalStake,
OperatorSetUniqueStake,
EigenDA,
Manual
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is manual? Just directed to anyone?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref: "Manual Distribution - Rewards that are sent directly to the Incentive Council multisig for manual distribution."

RewardsForAllEarners,
OperatorSetTotalStake,
OperatorSetUniqueStake,
EigenDA,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. If this becomes too complex in the EmissionsController, it might be worth just adding another createAVSRewardsSubmission with the AVS parameter in the RewardsCoordinator. There would be no sidecar changes here

RewardsForAllEarners,
OperatorSetTotalStake,
OperatorSetUniqueStake,
EigenDA,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we'd use the exact same event/logic. Don't think it's unreasonable bloat

@ypatil12
Copy link
Copy Markdown
Contributor

This should also target a release branch. release-dev/incentivesCouncil

@0xClandestine 0xClandestine changed the base branch from main to release-dev/incentive-council December 12, 2025 15:35

/// @title IEmissionsControllerEvents
/// @notice Events for the IEmissionsController contract.
interface IEmissionsControllerEvents is IEmissionsControllerTypes {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need an event for pressButton().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add an event with the implementation.

@0xClandestine 0xClandestine force-pushed the release-dev/incentive-council branch from 1564be7 to 1103809 Compare December 12, 2025 15:52
@0xClandestine 0xClandestine changed the title feat: draft IEmissionsController feat(incentives): add interface Dec 17, 2025
@0xClandestine 0xClandestine force-pushed the release-dev/incentive-council branch from 1103809 to 9e0aee8 Compare December 17, 2025 15:50
@0xClandestine 0xClandestine merged commit 730aecd into release-dev/incentive-council Dec 17, 2025
9 checks passed
@0xClandestine 0xClandestine deleted the feat/interface-design branch December 17, 2025 18:34
0xClandestine added a commit that referenced this pull request Jan 14, 2026
**Motivation:**

We need approval on initial design of the upcoming incentives council
feature release. Historically, we've used
[EigenHopper](https://github.com/Layr-Labs/EigenHopper/tree/master) for
token emissions. Now we're looking to make some improvements.

**Modifications:**

- Drafted a consolidated interface that combines the two `EigenHopper`
contracts into a single new contract.

**Result:**

Single consolidated interface with hopper functionality and proposed
ELIP-012 functionality.
eigenmikem added a commit that referenced this pull request Mar 2, 2026
# v1.12.0 Incentive Council (ELIP-012)

## Release Manager

@0xClandestine 

# Overview

**Core Features**

- **EmissionsController**: Mints EIGEN at a fixed inflation rate per
epoch and distributes via gauge weights (bips 0-10,000)
- **Permissionless Trigger**: Anyone can call `pressButton()` to process
epoch emissions—no trusted keeper required
- **5 Distribution Types**:
- `RewardsForAllEarners` — Protocol-wide rewards for all delegated stake
- `OperatorSetTotalStake` — Rewards proportional to total stake in
operator set
- `OperatorSetUniqueStake` — Rewards proportional to unique stake
allocations
  - `EigenDA` — Special pathway for EigenDA (pre-OperatorSets AVS)
- `Manual` — Off-chain computed distributions sent directly to
Incentives Committee
  
- **Silent Failure Handling**: Distribution failures don't block other
distributions (except reentrancy/OOG attacks)
- **Protocol Fee Mechanism**: Opt-in 20% fee on reward submissions in
RewardsCoordinator
  - Disabled by default (backward compatible)
  - Submitters opt-in via `setOptInForProtocolFee()`
  - Fee recipient configurable by owner

**Governance & Roles**

- **Protocol Council**: Sets Incentives Committee address via
`setIncentiveCouncil()`
- **Incentives Committee**: 
  - Configure distributions: `addDistribution()`, `updateDistribution()`
  - Receive swept tokens via `sweep()`
- **AVSs**: Must grant EmissionsController permission for OperatorSet
distributions

**Key Design Points**

- **Epoch-Based**: EIGEN minted once per epoch at
`EMISSIONS_INFLATION_RATE`
- **Future-Only Updates**: Distributions can only be added/updated for
future epochs
- **Immutable Config**: Inflation rate, start time, and epoch length set
at deployment
- **Pausable**: Both `pressButton()` and `sweep()` respect
`PAUSED_TOKEN_FLOWS` flag
- **Missed Epochs Skipped**: No accumulation—if `pressButton()` isn't
called during an epoch, those emissions are permanently lost

# Changelog

- feat(incentives): add interface
[#1678](#1678)
- feat(incentives): add implementation
[#1681](#1681)
- feat(incentives): add protocol fee
[#1691](#1691)
- feat(incentives): add deploy scripts
[#1699](#1699)
- fix: internal review changes
[#1703](#1703)
- feat: add EigenDA rewards submission type
[#1705](#1705)

# Scope

**New Contracts:**
- `EmissionsController.sol` — Main implementation
- `EmissionsControllerStorage.sol` — Storage layout
- `IEmissionsController.sol` — Interface

**Modified Contracts:**
- `RewardsCoordinator.sol` — Added protocol fee mechanism and
`createEigenDARewardsSubmission()`
- `RewardsCoordinatorStorage.sol` — Added `PROTOCOL_FEE_BIPS`,
`isOptedInForProtocolFee`, `feeRecipient`, `emissionsController`

---------

Co-authored-by: Rajath Alex <rajathalex@gmail.com>
Co-authored-by: eigenmikem <michael.muehl@eigenlabs.org>
Co-authored-by: ELHAJ <124453227+elhajin@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
0xClandestine added a commit that referenced this pull request Mar 5, 2026
# v1.12.0 Incentive Council (ELIP-012)

## Release Manager

@0xClandestine 

# Overview

**Core Features**

- **EmissionsController**: Mints EIGEN at a fixed inflation rate per
epoch and distributes via gauge weights (bips 0-10,000)
- **Permissionless Trigger**: Anyone can call `pressButton()` to process
epoch emissions—no trusted keeper required
- **5 Distribution Types**:
- `RewardsForAllEarners` — Protocol-wide rewards for all delegated stake
- `OperatorSetTotalStake` — Rewards proportional to total stake in
operator set
- `OperatorSetUniqueStake` — Rewards proportional to unique stake
allocations
  - `EigenDA` — Special pathway for EigenDA (pre-OperatorSets AVS)
- `Manual` — Off-chain computed distributions sent directly to
Incentives Committee
  
- **Silent Failure Handling**: Distribution failures don't block other
distributions (except reentrancy/OOG attacks)
- **Protocol Fee Mechanism**: Opt-in 20% fee on reward submissions in
RewardsCoordinator
  - Disabled by default (backward compatible)
  - Submitters opt-in via `setOptInForProtocolFee()`
  - Fee recipient configurable by owner

**Governance & Roles**

- **Protocol Council**: Sets Incentives Committee address via
`setIncentiveCouncil()`
- **Incentives Committee**: 
  - Configure distributions: `addDistribution()`, `updateDistribution()`
  - Receive swept tokens via `sweep()`
- **AVSs**: Must grant EmissionsController permission for OperatorSet
distributions

**Key Design Points**

- **Epoch-Based**: EIGEN minted once per epoch at
`EMISSIONS_INFLATION_RATE`
- **Future-Only Updates**: Distributions can only be added/updated for
future epochs
- **Immutable Config**: Inflation rate, start time, and epoch length set
at deployment
- **Pausable**: Both `pressButton()` and `sweep()` respect
`PAUSED_TOKEN_FLOWS` flag
- **Missed Epochs Skipped**: No accumulation—if `pressButton()` isn't
called during an epoch, those emissions are permanently lost

# Changelog

- feat(incentives): add interface
[#1678](#1678)
- feat(incentives): add implementation
[#1681](#1681)
- feat(incentives): add protocol fee
[#1691](#1691)
- feat(incentives): add deploy scripts
[#1699](#1699)
- fix: internal review changes
[#1703](#1703)
- feat: add EigenDA rewards submission type
[#1705](#1705)

# Scope

**New Contracts:**
- `EmissionsController.sol` — Main implementation
- `EmissionsControllerStorage.sol` — Storage layout
- `IEmissionsController.sol` — Interface

**Modified Contracts:**
- `RewardsCoordinator.sol` — Added protocol fee mechanism and
`createEigenDARewardsSubmission()`
- `RewardsCoordinatorStorage.sol` — Added `PROTOCOL_FEE_BIPS`,
`isOptedInForProtocolFee`, `feeRecipient`, `emissionsController`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants